This is part 3 of our blog post series about Thorgate’s new SPA project template. So far, we have generated a new project and implemented showing parrots to the user. We used Redux Saga to retrieve the parrots and Django Rest Framework for creating the API.
Here are the links to the other posts:
In this article we’ll implement adding parrots using Formik and Redux Saga.
Creating parrots
Currently, the only way to add parrots is via the admin page. This is a bit of a problem since then only admin users can add parrots. Also, it’s quite inconvenient to navigate to the “add a parrot” admin page. So, let’s create a new view with a form for creating parrots.
Let’s add a simple non-functional form view to views/CreateParrot.js
:
# app/src/views/CreateParrot.js
import React from 'react';
import { Form, FormGroup, Label, Input, Button, Container } from 'reactstrap';
const CreateParrot = () => (
<Container>
<h1>Add a new parrot!</h1>
<Form>
<FormGroup>
<Label for="name">Name</Label>
<Input name="name" id="name" />
</FormGroup>
<FormGroup>
<Label for="link">Link</Label>
<Input name="link" id="link" type="url" />
</FormGroup>
<Button>Create</Button>
</Form>
</Container>
);
export default CreateParrot;
And add a route for it in configuration/routes.js
:
# app/src/configuration/routes.js
const CreateParrot = loadable(() => import('views/CreateParrot'));
...
{
path: '/parrots/create',
exact: true,
name: 'create-parrots',
component: CreateParrot,
},
You should see a page like this at /parrots/create
:
Unfortunately, we don’t currently have access to any field values that the user inputs. One option would be to convert the component to a class component and keep track of the name and the link in the component’s state. Another option would be to use refs and manually query the name and link inputs for their values when the user submits the form. Hooks could also be used instead of converting the component to a class component.
Those solutions require a lot of manual work though (especially for larger forms). So, let’s use a package that makes forms in React a breeze: Formik. We can convert our component to use Formik like so:
# app/src/views/CreateParrot.js
import React from 'react';
import { FormGroup, Label, Input, Button, Container } from 'reactstrap';
import { Formik, Form } from 'formik';
const CreateParrot = () => (
<Container>
<h1>Add a new parrot!</h1>
<Formik
initialValues={{ name: '', link: '' }}
onSubmit={(values) => {
console.log('Submitting!', values);
}}
>
{({ values, handleChange }) => (
<Form>
<FormGroup>
<Label for="name">Name</Label>
<Input
name="name"
id="name"
value={values.name}
onChange={handleChange}
/>
</FormGroup>
<FormGroup>
<Label for="link">Link</Label>
<Input
name="link"
id="link"
type="url"
value={values.link}
onChange={handleChange}
/>
</FormGroup>
<Button type="submit">Create</Button>
</Form>
)}
</Formik>
</Container>
);
export default CreateParrot;
Formik keeps track of the form’s state internally and uses a render prop-based API to allow us to access the state and modify it. It is simple to add validation, show validation errors and handle server errors. Even things like disabling the submit button when the user has clicked it is quite straight-forward.
You should now see a message logged to the console with the values you inputted.
Now, we need to send this data to the server so that the server can store the parrot in the database. A very simple way to do it would be to just send a request using fetch
in the onSubmit
handler, but let’s use a saga for that.
Let’s add a createParrot.js
file to sagas/parrots/
with the following contents:
# app/src/sagas/parrots/createParrot.js
import { takeEvery } from 'redux-saga/effects';
const CREATE_PARROT = 'CREATE_PARROT';
export const createParrot = (name, link) => ({
type: CREATE_PARROT,
name,
link,
});
function* createParrotSaga(createParrotAction) {
console.log('create parrot:', createParrotAction);
}
export default function* createParrotWatcher() {
yield takeEvery(CREATE_PARROT, createParrotSaga);
}
This saga specifies its own action that it can react to — CREATE_PARROT
. The createParrotWatcher
will be watching for any dispatched CREATE_PARROT
actions and run createParrotSaga
passing it the action as the argument. Currently we’re just logging the createParrotAction
in order to ensure that we have the correct data available. Let’s configure this Saga to “watch” for any actions dispatched in the /parrots/create
route by adding it as the watcher
for that route in routes.js
:
# app/src/configuration/routes.js
import createParrotWatcher from 'sagas/parrots/createParrot';
...
{
path: '/parrots/create',
exact: true,
name: 'create-parrots',
component: CreateParrot,
watcher: createParrotWatcher,
},
And finally, let’s connect our CreateParrot
component to Redux:
# app/src/views/CreateParrot.js
...
import { connect } from 'react-redux';
import { createParrot } from 'sagas/parrots/createParrot';
const CreateParrot = ({ createParrot }) => (
<Container>
<h1>Add a new parrot!</h1>
<Formik
initialValues={{ name: '', link: '' }}
onSubmit={(values) => {
createParrot(values.name, values.link);
}}
>
...
</Formik>
</Container>
);
const mapDispatchToProps = (dispatch) => ({
createParrot: (name, link) => dispatch(createParrot(name, link)),
});
const CreateParrotConnector = connect(
null,
mapDispatchToProps,
)(CreateParrot);
export default CreateParrotConnector;
Now, whenever the form is submitted, we dispatch the CREATE_PARROT
action passing it the name and the link.
We should now see the message logged in the console with the createParrotAction
including the specified name
and link
.
We can now send a POST request to the server using our parrots.list
resource:
# app/src/sagas/parrots/createParrot.js
...
import api from 'services/api';
...
function* createParrotSaga(createParrotAction) {
try {
const { name, link } = createParrotAction;
yield api.parrots.list.post(null, { name, link });
} catch (e) {
console.log('Something went wrong!');
}
}
...
Attaching the authenticated user
This almost works. We aren’t currently sending the user_id
field to the server, but as it’s required, we get a 500
error saying that the user_id
field cannot be null
. It would be great if we didn’t need to send this user_id
manually, but instead, Django would take the authenticated user and save the parrot to the database with that user’s ID.
Luckily for us, we can accomplish this very easily by overriding the perform_create
method on the ParrotViewSet
and passing the currently authenticated user to the serializer:
# parrot_mania/parrots/views.py
class ParrotViewSet(viewsets.ModelViewSet):
...
def perform_create(self, serializer):
return serializer.save(user=self.request.user)
Creating parrots should now work. However, we don’t actually see anything happening. Let’s redirect the user to /parrots
once the request has finished. In the createParrot
Saga, modify the createParrotSaga
function like this:
# app/src/sagas/parrots/createParrot.js
import { push } from 'connected-react-router';
import { takeEvery, put } from 'redux-saga/effects';
...
function* createParrotSaga(createParrotAction) {
try {
const { name, link } = createParrotAction;
yield api.parrots.list.post(null, { name, link });
yield put(push('/parrots'));
} catch (e) {
console.log('Something went wrong!');
}
}
...
Let’s also add a link to /parrots/create
to our ParrotsList
component:
# app/src/views/ParrotsList.js
...
import { Link } from 'react-router-dom';
...
const ParrotsList = ({ parrots }) => (
<Container>
<h1>Here are your parrots!</h1>
<Link to="/parrots/create">
Add a new parrot!
</Link>
...
</Container>
);
Here’s how the application should look now:
The functionality I planned for Parrot-mania is now complete but if you’re eager to continue working on this project, try adding updating and deleting parrots as well in order to make it a full CRUD app.
You can check out the full source of Parrot-mania here: https://github.com/JoosepAlviste/parrot-mania.
Conclusion
We created a simple parrots’ management web app using some interesting technologies included in Thorgate’s SPA project template. Some of those to highlight, in my opinion, are:
- Docker makes it possible to run the application with minimal set-up
- Server-side rendering allows us to write the UI layer of our application in React while not having to worry about search engines
- Formik is a great package for writing forms in a super-simple way
- Redux Saga is used to handle side effects in an organized manner
There is also a Medium post where you can leave your feedback or other comments: